package com.hm.health.Utils;

import android.content.Context;
import android.os.Environment;
import android.util.Log;

import com.goodix.utils.FileSaveUtil;
import com.goodix.utils.SPUtils;
import com.goodix.entity.HCIFilterParam;
import com.hm.health.MyApplication;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Set;

/**
 * Created by jiaguang on 2018/11/6.
 */

public class HciFileHeartRateFetcher {
    /**
     * 从HCI文件中的获取包含时间戳的心率信息
     */

    private static final String TAG = "HciFileHeartRateFetcher";
    private static final String RESULT_FILE_PATH = "hci_result";
    private static final String RESULT_FILE_NAME = "hci_heart_rate.csv";
    private static final String FILE_PATH_NAME = Environment.getExternalStorageDirectory()
            + "/Android/data/btsnoop_hci.log";

    public static final int HCI_FILE_COMBINING_SUCCESS = 0;
    public static final int HCI_FILE_COMBINING_FAILED = -1;
    public static final int HCI_DEVICE_WRONG = -2;
    public static final int HCI_FILE_NOT_FOUND = -3;
    public static final int IO_EXCEPTION = -4;

    // 单次读取的字节数
    public static final int READ_BLOCK_SIZE = 200000;
    private static final long BTSNOOP_EPOCH_DELTA = 0x00dcddb30f2f8000L;


    public static final String HCI_DEVICE = "hci_device";
    public static final String DEVICE_NO = "NoDevice";

    private int mSizeOfNotify = 0;
    private int mHeartRateOffset = 0;
    private byte[] mFilter;
    private File mFile;
    private File mFileCopy;
    private FileInputStream mFileInputStream;
    private FileOutputStream mFileOutputStream;
    private byte[] mByteBuffer = new byte[READ_BLOCK_SIZE];
    private int mValidLength;
    private int mByteNotUsed = 0;
    private long mLastTime;
    private LinkedHashMap<Long, Integer> mTimeAndHeartRateMap;
    private LinkedHashMap<Long, Integer> mTimeAndHeartRateMapTemp;
    private FileSaveUtil mFileSaveUtil;
    private SimpleDateFormat mDateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS");
    private Context mContext;

    private boolean mIsNeedInsert = true;
    public void setIsNeedInsert(boolean isNeedInsert){
        mIsNeedInsert = isNeedInsert;
    }
    private HciCombineCallback mHciCombineCallback;
    private int mFileSize;
    private int mByteRead;

    public HciFileHeartRateFetcher(HciCombineCallback hciCombineCallback, Context context){
        mHciCombineCallback = hciCombineCallback;
        mContext = context;
    }

    public static boolean isNeedCombineFile(Context context){
        String currentHciDevice = getHciDevice(context);
        return  !HciFileHeartRateFetcher.DEVICE_NO.equals(currentHciDevice);
    }

    public LinkedHashMap<Long, Integer> getAndSaveTimeAndHeartRate(){
        mLastTime = 0;
        mByteNotUsed = 0;
        getFilterInfo();
        if(null == mFilter || 0 == mSizeOfNotify){
            Log.e(TAG, "filter not defined!!!");
            onProgress(HCI_DEVICE_WRONG);
            return null;
        }
        startSave();
        mFile = new File(FILE_PATH_NAME);
        mFileCopy = new File(new File(mFileSaveUtil.getFullPath()).getParent() + "/" + mFile.getName());
        mTimeAndHeartRateMap = new LinkedHashMap<>();
        try{
            if(!mFileCopy.exists()){
                mFileCopy.createNewFile();
            }
            mFileInputStream = new FileInputStream(mFile);
            mFileOutputStream = new FileOutputStream(mFileCopy);
            mFileSize = mFileInputStream.available();
            mByteRead = 0;
            while((mValidLength = getByte()) > mByteNotUsed) {
                if(0 == mByteRead){
                    mByteRead += mValidLength;
                    mFileOutputStream.write(mByteBuffer, 0, mValidLength);
                } else {
                    mByteRead += (mValidLength - mByteNotUsed);
                    mFileOutputStream.write(mByteBuffer, mByteNotUsed, mValidLength - mByteNotUsed);
                }
                onProgress((int)(100.0 * mByteRead / mFileSize));
                mTimeAndHeartRateMapTemp = getTimeAndHeartRate(mByteBuffer);
                saveResultToFile(mTimeAndHeartRateMapTemp);
                mTimeAndHeartRateMap.putAll(mTimeAndHeartRateMapTemp);
            }
        } catch (FileNotFoundException e){
            Log.e(TAG, "FileNotFoundException");
            onProgress(HCI_FILE_NOT_FOUND);
            return null;
        } catch (IOException e){
            Log.e(TAG, "IOException");
            onProgress(IO_EXCEPTION);
            return null;
        } finally {
            try {
                if(null != mFileInputStream) {
                    mFileInputStream.close();
                }
                if(null != mFileOutputStream) {
                    mFileOutputStream.flush();
                    mFileOutputStream.close();
                }
            } catch (IOException e){
                e.printStackTrace();
                Log.e(TAG, "close mFileInputStream failed!!!");
            }
            finishSave();
        }
        return mTimeAndHeartRateMap;
    }

    private void onProgress(int percents){
        if(null != mHciCombineCallback){
            Log.d(TAG, "percents: " + percents);
            mHciCombineCallback.onProgress(percents);
        }
    }

    private void getFilterInfo(){
        String deviceName = getHciDevice(mContext);
        HCIFilterParam hciFilterParam = MyApplication.getHbdOptionIni().getHCIFilterByName(deviceName);
        mFilter = hciFilterParam.getFilter();
        mSizeOfNotify = hciFilterParam.getSizeOfNotify();
        mHeartRateOffset = hciFilterParam.getHeartRateOffset();
    }

    public static String getHciDevice(Context context){
        if(null != context) {
            return (String) SPUtils.get(context, HCI_DEVICE, DEVICE_NO);
        } else {
            return DEVICE_NO;
        }
    }

    public static void putHciDevice(Context context, String hciDevice){
        if(null != context) {
            SPUtils.put(context, HCI_DEVICE, hciDevice);
        }
    }

    private int getByte() throws IOException{
        try {
            if (0 == mByteNotUsed) {
                mByteNotUsed = mSizeOfNotify - 1;
                return mFileInputStream.read(mByteBuffer);
            } else {
                if(mValidLength <= mByteNotUsed){
                    return 0;
                } else {
                    System.arraycopy(mByteBuffer, mValidLength - mByteNotUsed, mByteBuffer, 0, mByteNotUsed);
                    return mByteNotUsed + mFileInputStream.read(mByteBuffer, mByteNotUsed, mByteBuffer.length - mByteNotUsed);
                }
            }
        } catch (Exception e){
            e.printStackTrace();
            Log.e(TAG, e.toString());
            return 0;
        }
    }

    private void startSave(){
        mFileSaveUtil = new FileSaveUtil(RESULT_FILE_PATH, mDateFormat.format(new Date()) + RESULT_FILE_NAME);
        mFileSaveUtil.startSave();
    }

    private void finishSave(){
        mFileSaveUtil.finishedSave();
    }

    private void saveResultToFile(LinkedHashMap<Long, Integer> timeAndHeartRateMap){
        Set<Long> keys = timeAndHeartRateMap.keySet();
        Iterator<Long> key = keys.iterator();
        long time;
        String timeInfo;
        int heartRate;
        while(key.hasNext()){
            time = key.next();
            timeInfo = mDateFormat.format(time);
            heartRate = timeAndHeartRateMap.get(time);
            if(mTimeAndHeartRateMap.containsKey(time)){
                Log.d(TAG, "already_contains_key: " + time + " " + timeInfo);
                continue;
            }
            mFileSaveUtil.saveData(time + "," + timeInfo + "," + heartRate + "\n");
        }
    }

    private LinkedHashMap<Long, Integer> getTimeAndHeartRate(byte[] bytes){
        LinkedHashMap<Long, Integer> timeAndHeartRate = new LinkedHashMap<>();
        boolean isFound;
        for(int indexOuter = 0; indexOuter < mValidLength - mSizeOfNotify + 1; indexOuter++){
            isFound = true;
            for(int indexInner = 0; indexInner < mFilter.length; indexInner++){
                if(0 == mFilter[indexInner]){
                    continue;
                }
                if(bytes[indexOuter + indexInner] != mFilter[indexInner]){
                    isFound = false;
                    break;
                }
            }
            if(isFound){
                long time = 0;
                for(int timePos = 8; timePos > 0; --timePos){
                    long fragment = bytes[indexOuter - timePos] & 0xff;
                    int bitToShift = 8 * (timePos - 1);
                    time += (fragment << bitToShift);
                }
                long timeStamp = getHCITimeStampEpoch(time);
                if(mIsNeedInsert) {
                    if (0 != mLastTime) {
                        long diffTime = (timeStamp - mLastTime) / 1000;
                        if (diffTime > 1) {
                            Integer rate = timeAndHeartRate.get(mLastTime);
                            if (null == rate) {
                                rate = mTimeAndHeartRateMap.get(mLastTime);
                            }
                            for (long currentTime = mLastTime; currentTime < timeStamp; currentTime += 1000) {
                                timeAndHeartRate.put(currentTime, rate);
                            }
                        }
                    }
                }
                mLastTime = timeStamp;
                Log.d(TAG, "timeStamp: " + timeStamp);
                int heartRate = 0xff & bytes[indexOuter + mHeartRateOffset];
                indexOuter += mSizeOfNotify;
                timeAndHeartRate.put(timeStamp, heartRate);
            }
        }
        return timeAndHeartRate;
    }

    private long getHCITimeStampEpoch(long time){
        long v = (time - BTSNOOP_EPOCH_DELTA - 28800000000L);
        long f2 = v / 1000 / 1000;
        return f2 * 1000;
    }

    public interface HciCombineCallback {
        void onProgress(int percents);
        void onCombineResult(int result);
    }
}
